/******************************************************************************
 * Copyright (c) 2016 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 ******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.services;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;

import org.eclipse.lsp4j.jsonrpc.Endpoint;
import org.eclipse.lsp4j.jsonrpc.services.AnnotationUtil.DelegateInfo;
import org.eclipse.lsp4j.jsonrpc.services.AnnotationUtil.MethodInfo;

/**
 * A Proxy that wraps an {@link Endpoint} in one or more service interfaces, i.e. interfaces
 * containing {@link JsonNotification} and {@link JsonRequest} methods.
 */
public class EndpointProxy implements InvocationHandler {

	private final Method object_equals;
	private final Method object_hashCode;
	private final Method object_toString;

	private final Endpoint delegate;
	private final LinkedHashMap<String, MethodInfo> methodInfos;
	private final LinkedHashMap<String, DelegateInfo> delegatedSegments;

	public EndpointProxy(Endpoint delegate, Class<?> interface_) {
		this(delegate, Collections.singletonList(interface_));
	}

	public EndpointProxy(Endpoint delegate, Collection<Class<?>> interfaces) {
		if (delegate == null)
			throw new NullPointerException("delegate");
		if (interfaces == null)
			throw new NullPointerException("interfaces");
		if (interfaces.isEmpty())
			throw new IllegalArgumentException("interfaces must not be empty.");

		this.delegate = delegate;
		try {
			object_equals = Object.class.getDeclaredMethod("equals", Object.class);
			object_hashCode = Object.class.getDeclaredMethod("hashCode");
			object_toString = Object.class.getDeclaredMethod("toString");
		} catch (NoSuchMethodException | SecurityException exception) {
			throw new RuntimeException(exception);
		}
		methodInfos = new LinkedHashMap<>();
		delegatedSegments = new LinkedHashMap<>();
		for (Class<?> interf : interfaces) {
			AnnotationUtil.findRpcMethods(interf, new HashSet<>(), methodInfo -> {
				if (methodInfos.put(methodInfo.method.getName(), methodInfo) != null) {
					throw new IllegalStateException("Duplicate RPC method " + methodInfo.method);
				}
			});
			AnnotationUtil.findDelegateSegments(interf, new HashSet<>(), method -> {
				Object delegateProxy = ServiceEndpoints.toServiceObject(delegate, method.getReturnType());
				final var info = new DelegateInfo();
				info.delegate = delegateProxy;
				info.method = method;
				if (delegatedSegments.put(method.getName(), info) != null) {
					throw new IllegalStateException("Duplicate RPC method " + method);
				}
			});
		}
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		args = args == null ? new Object[0] : args;
		MethodInfo methodInfo = this.methodInfos.get(method.getName());
		if (methodInfo != null) {
			Object params = getParams(args, methodInfo);
			if (methodInfo.isNotification) {
				delegate.notify(methodInfo.name, params);
				return null;
			}
			return delegate.request(methodInfo.name, params);
		}
		DelegateInfo delegateInfo = this.delegatedSegments.get(method.getName());
		if (delegateInfo != null) {
			return delegateInfo.delegate;
		}
		if (object_equals.equals(method) && args.length == 1) {
			if(args[0] != null ) {
				try {
					return this.equals(Proxy.getInvocationHandler(args[0]));
				} catch (IllegalArgumentException exception) {
				}
			}
			return this.equals(args[0]);
		}
		if (object_hashCode.equals(method)) {
			return this.hashCode();
		}
		if (object_toString.equals(method)) {
			return this.toString();
		}
		return method.invoke(delegate, args);
	}

	protected Object getParams(Object[] args, MethodInfo methodInfo) {
		if (args.length == 0) {
			return null;
		}
		if (args.length == 1) {
			return args[0];
		}
		return Arrays.asList(args);
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + " for " + delegate.toString();
	}

}
